﻿using System;
using System.Collections.Concurrent;
using System.Diagnostics.Contracts;
using System.IO;
using System.Threading;
using O1.Kernel.Primitives;

namespace O1.Kernel.Storage
{
    [ContractVerification(true)]
    internal class StorageSequence
    {
        private readonly ConcurrentQueue<long> deleted;
        
        private long counter;

        public StorageSequence(long initialNextId, long[] deletedIds)
        {
            Contract.Requires(deletedIds != null);
            Contract.Requires(initialNextId > -1);

            Contract.Ensures(this.deleted != null);

            this.counter = initialNextId;
            this.deleted = new ConcurrentQueue<long>(deletedIds);
        }

        public StorageSequence()
        {
            this.deleted = new ConcurrentQueue<long>();
        }

        public static bool TryDecodeIds(Stream stream, out long initialNextId, out long[] deletedIds)
        {
            Contract.Requires(stream != null);

            deletedIds = null;
            initialNextId = -1;
            byte[] buffer = new byte[Sz.CLR.Int32];
            var bytesRead = stream.Read(buffer, 0, Sz.CLR.Int32);
            if (bytesRead != Sz.CLR.Int32)
            {
                return false;
            }

            int count = buffer.ToInt32(0);
            if (count < 1)
            {
                return false;
            }

            buffer = new byte[count * Sz.CLR.Int64];
            bytesRead = stream.Read(buffer, 0, buffer.Length);
            if (bytesRead != buffer.Length)
            {
                return false;
            }

            initialNextId = buffer.ToInt64(0);
            deletedIds = new long[count - 1];

            Buffer.BlockCopy(buffer, Sz.CLR.Int64, deletedIds, 0, (count - 1) * Sz.CLR.Int64);
            return true;
        }

        public long NextId()
        {
            Contract.Ensures(Contract.Result<long>() > -1);

            long targetId;
            if (!this.deleted.TryDequeue(out targetId))
            {
                targetId = Interlocked.Increment(ref this.counter);
                Contract.Assume(this.counter > -1);
            }

            Contract.Assume(targetId > -1);
            return targetId;
        }

        public void Recycle(long id)
        {
            Contract.Requires(id > -1);

            this.deleted.Enqueue(id);
        }

        public virtual void EncodeIds(Stream stream)
        {
            Contract.Requires(stream != null);

            var buffer = new byte[(this.deleted.Count * Sz.CLR.Int64) + Sz.CLR.Int64 + Sz.CLR.Int32];
            
            // Write the count as 4 bytes:
            (this.deleted.Count + 1).CopyBytes(buffer, 0);

            // Write the next ID as 8 bytes:
            this.NextId().CopyBytes(buffer, Sz.CLR.Int32);

            // Next position is 12...roll through the deleted Ids.
            var position = Sz.CLR.Int32 + Sz.CLR.Int64;
            foreach (var value in this.deleted)
            {
                Contract.Assume(buffer.Length > position + Sz.CLR.Int64);
                value.CopyBytes(buffer, position);
                position += Sz.CLR.Int64;
            }

            stream.Write(buffer, 0, buffer.Length);
        }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(this.deleted != null);
            Contract.Invariant(this.counter > -1);
        }
    }
}